﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LiftEmulator.Model
{
    /// <summary>
    /// Лифт.    
    /// </summary>
    public class Lift
    {
        #region Поля и свойства класса
        /// <summary>
        /// Перечень возможных состояний
        /// </summary>
        public enum LiftState
        {
            Stop,
            Up,
            Down
        }

        /// <summary>
        /// Максимальная вместимость лифта
        /// </summary>
        public readonly int MaxCapacityOfTheElevator;

        /// <summary>
        /// Текущее состояние
        /// </summary>
        private LiftState state = LiftState.Stop;        

        /// <summary>
        /// Содержимое лифта
        /// </summary>
        private List<People> inner = new List<People>();        

        /// <summary>
        /// Этаж, на котором находится лифт
        /// </summary>
        public Floor CurrentFloor { get; private set; }

        /// <summary>
        /// Здание, в которое установлен лифт
        /// Лифту необходим доступ к кнопкам
        /// </summary>
        public Building Building { get; private set; }

        #endregion

        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="building">здание, в котором установлен лифт</param>
        /// <param name="maxCapacityOfTheElevator">вместимость</param>
        public Lift(Building building, int maxCapacityOfTheElevator)
        {            
            Building = building;
            this.MaxCapacityOfTheElevator = maxCapacityOfTheElevator;
            CurrentFloor = Building.Floors.First();            
        }
               

        /// <summary>
        /// Метод содержит логику управления лифтом.
        /// Содержит только управляющую логику, всё остальное вынесено в дополнительные методы/свойства
        /// </summary>
        public void Step()
        {
            var prevStep = state; // необходимо для формирования уведомления

            switch (state)
            {
                case LiftState.Stop:

                    if (AnyPeopleWantsOutOnThisFloor) 
                    {
                        UnBoardingOne();
                    }
                    else if (CurrentFloor.IsButtonPushed && !IsFull)
                    {
                        BoardingOne();
                    }
                    else if (inner.Any())
                    {
                        state = DemocracyChooseFloorIsUpper() ? LiftState.Up : LiftState.Down;
                    }
                    else 
                    {
                        var targetFloor = FindFloorWithButtonPushed();
                        if (targetFloor != null)
                        {
                            state = targetFloor.Number > CurrentFloor.Number ? LiftState.Up : LiftState.Down;
                        }
                        else
                        {
                            state = LiftState.Stop;
                        }
                    }
                break;

                case LiftState.Up:
                case LiftState.Down:
                    var prevFloor = CurrentFloor; // для уведомления
                    
                    CurrentFloor = GetNextFloorByState();

                    RaiseChangeFloorEvent(prevFloor);
                  
                    if (AnyPeopleWantsOutOnThisFloor)
                    {                        
                        state = LiftState.Stop;
                    }                    
                    
                    if (!IsFull && CurrentFloor.IsButtonPushed)
                    {
                        state = LiftState.Stop;
                    }                  
                   
                break;

                default: throw new NotImplementedException(string.Format("Состояние {0} не обрабатывается", state));
            }

            RaiseChangeStateEvent(prevStep);
        }
                

        #region Дополнительные свойства и методы

        /// <summary>
        /// Содержимое лифта, для внешних пользователей (защищаем от изменения)
        /// </summary>
        public IEnumerable<People> Inner
        {
            get
            {
                return inner;
            }
        }

        /// <summary>
        /// Лифт полный?
        /// </summary>
        private bool IsFull
        {
            get
            {
                return inner.Count == MaxCapacityOfTheElevator;
            }
        }


        /// <summary>
        /// Есть ли желающий выйти на текущем этаже
        /// </summary>
        private bool AnyPeopleWantsOutOnThisFloor
        {
            get
            {
                return inner.Any(people => people.TargetFloor == CurrentFloor);
            }
        }


        /// <summary>
        /// Войти в лифт
        /// </summary>
        private void BoardingOne()
        {
            inner.Add(CurrentFloor.Dequeue());
            RaiseBoardingEvent(true);
        }

        
        /// <summary>
        /// Выйти из лифта
        /// </summary>
        private void UnBoardingOne()
        {
            inner.Remove(inner.First(people => people.TargetFloor == CurrentFloor));
            RaiseBoardingEvent(false);
        }

        /// <summary>
        ///  демократический лифт, едем туда, куда хочет большинство (если одинаково, то первый попавшийся)
        ///  причем учитываем, так как люди не могут хотеть на этаж выше или ниже максимального/минимального, то
        ///  можно доп проверок не ставить
        /// </summary>
        /// <returns>true - если этаж находится выше текущего, false - если ниже</returns>
        private bool DemocracyChooseFloorIsUpper()
        {
            Func<People, bool> isUpper = people => (people.TargetFloor.Number > CurrentFloor.Number);

            return inner
                .GroupBy(isUpper)
                .OrderByDescending(group => group.Count())
                .Select(group => group.Key)
                .First();
        }

        /// <summary>
        /// Найти этаж, где нажата кнопка
        /// </summary>
        /// <returns>этаж или null</returns>
        private Floor FindFloorWithButtonPushed()
        {
            return Building.Floors.Where(floor => floor != CurrentFloor).FirstOrDefault(floor => floor.IsButtonPushed);
        }

        /// <summary>
        /// Получить следующий этаж, в зависимости от текущего состояния.
        /// up - следующий выше этаж
        /// down - следующий ниже этаж
        /// </summary>
        /// <returns>следующий этаж</returns>
        private Floor GetNextFloorByState()
        {
            switch (state)
            {
                case LiftState.Up: return Building.UpFloor(CurrentFloor);
                case LiftState.Down: return Building.DownFloor(CurrentFloor);
                default: throw new InvalidOperationException("Для других состояний нельзя определить следующий этаж");
            }
        }
        #endregion

        #region Событие ChangeState (изменение состояния)

        /// <summary>
        /// Аргументы события изменения состояния
        /// </summary>
        public class ChangeStateEventArgs : EventArgs
        {
            /// <summary>
            /// Предыдущее состояние
            /// </summary>
            public Lift.LiftState PrevState { get; private set; }
            
            /// <summary>
            /// Новое состояние
            /// </summary>
            public Lift.LiftState NowState { get; private set; }

            /// <summary>
            /// Конструктор
            /// </summary>
            /// <param name="prevState">предыдущее состояние</param>
            /// <param name="nowState">новое состояние</param>
            public ChangeStateEventArgs(Lift.LiftState prevState, Lift.LiftState nowState)
            {
                this.PrevState = prevState;
                this.NowState = nowState;
            }
        }

        /// <summary>
        /// Событие изменение состояния
        /// </summary>
        public event EventHandler<ChangeStateEventArgs> ChangeState;
        
        /// <summary>
        /// Вызов события изменения состояния
        /// </summary>
        /// <param name="prevStep">предыдущее состояние</param>
        private void RaiseChangeStateEvent(LiftState prevStep)
        {
            var changeState = ChangeState;
            if (changeState != null) changeState(this, new ChangeStateEventArgs(prevStep, state));
        }

        #endregion

        #region Событие Boarding (загрузка/выгрузка из лифта)

        /// <summary>
        /// Аргументы события загрузки/выгрузки из лифта
        /// </summary>
        public class BoardingEventArgs : EventArgs
        {
            /// <summary>
            /// true - загрузка, false - выгрузка
            /// </summary>
            public bool IsBoarding { get; private set; }

            /// <summary>
            /// Текущее содержимое очереди
            /// </summary>
            public IEnumerable<People> Inner { get; private set; }

            /// <summary>
            /// Конструктор
            /// </summary>
            /// <param name="isBoarding">true - загрузка, false - выгрузка</param>
            /// <param name="inner">Текущее содержимое очереди</param>
            public BoardingEventArgs(bool isBoarding, IEnumerable<People> inner)
            {
                this.IsBoarding = isBoarding;
                this.Inner = inner;
            }
        }

        /// <summary>
        /// Событие загрузки/выгрузки из лифта
        /// </summary>
        public event EventHandler<BoardingEventArgs> Boarding;

        /// <summary>
        /// Вызов события загрузки/выгрузки из лифта
        /// </summary>
        /// <param name="isBoarding">true - загрузка, false - выгрузка</param>
        private void RaiseBoardingEvent(bool isBoarding)
        {
            var boarding = Boarding;
            if (boarding != null) boarding(this, new BoardingEventArgs(isBoarding, inner));
        }

        #endregion

        #region Событие ChangeFloor (перемещение лифта на следующий этаж)

        /// <summary>
        /// Аргументы события перемещение лифта на следующий этаж
        /// </summary>
        public class ChangeFloorEventArgs : EventArgs
        {
            /// <summary>
            /// Предыдущий этаж
            /// </summary>
            public Floor PrevFloor { get; private set; }
            
            /// <summary>
            /// Текущий этаж
            /// </summary>
            public Floor CurrentFloor { get; private set; }

            /// <summary>
            /// Конструктор
            /// </summary>
            /// <param name="prevFloor">предыдущий этаж</param>
            /// <param name="currentFloor">текущий этаж</param>
            public ChangeFloorEventArgs(Floor prevFloor, Floor currentFloor)
            {
                this.PrevFloor = prevFloor;
                this.CurrentFloor = currentFloor;
            }
        }

        /// <summary>
        /// Событие перемещение лифта на следующий этаж
        /// </summary>
        public event EventHandler<ChangeFloorEventArgs> ChangeFloor;

        /// <summary>
        /// Вызов события перемещения лифта на следующий этаж
        /// </summary>
        /// <param name="prevFloor">предыдущий этаж</param>
        private void RaiseChangeFloorEvent(Floor prevFloor)
        {
            var changeFloor = ChangeFloor;
            if (changeFloor != null) changeFloor(this, new ChangeFloorEventArgs(prevFloor, CurrentFloor));
        }
        #endregion
    }
}
